#include <iostream>
#include <cstring>
#include <fstream>
#include <vector>
#include <limits>
#include <iomanip>

#include "Arguments.h"
#include "XMLParser.h"
#include "Algorithm.h"


namespace gpxtools
{
  const double INVALID = std::numeric_limits<double>::min();

  class GPXLs : public XMLParserHandler
  {
  public:
    // -- Constructor -----------------------------------------------------------
    GPXLs() :
      _arguments("gpxls [OPTION].. [FILE]..\nList summary or full information from GPX-files.\n", "gpxls v0.2", "List the contents of GPX-files"),
      _summary(_arguments, true,  's', "summary",                   "display summary information"),
      _full(_arguments,    true,  'f', "full",                      "display full information with elevation (m), time and distance (m)"),
      _extend(_arguments,  true,  'e', "extend",                    "extend the full display with the calculated bearing and speed (km/h)"),
      _xmlParser(this),
      _waypoints(),
      _routes(),
      _tracks()
    {

    }

    // -- Deconstructor----------------------------------------------------------
    virtual ~GPXLs()
    {

    }

    // -- Properties ------------------------------------------------------------

    // -- Parse arguments -------------------------------------------------------
    bool parseArguments(int argc, char *argv[])
    {
      std::vector<std::string> filenames;

      if (!_arguments.parse(argc,argv, filenames))
      {
        return false;
      }
      else if (!checkArguments())
      {
        return false;
      }
      else if (filenames.empty())
      {
        return _xmlParser.parse(std::cin);
      }
      else
      {
        for (auto filename = filenames.begin(); filename != filenames.end(); ++filename)
        {
          if (!parseFile(*filename)) return false;
        }
      }
      return true;
    }

    // -- Check arguments ---------------------------------------------------------
    bool checkArguments()
    {
      if (_summary.active() && _full.active())
      {
        _summary.active(false);
      }
      else if (!_summary.active() && !_full.active())
      {
        _summary.active(true);
      }

      return true;
    }

    // -- Parse a file ----------------------------------------------------------
    bool parseFile(const std::string &filename)
    {
      bool ok =false;

      std::ifstream file(filename);

      if (file.is_open())
      {
        ok = _xmlParser.parse(file);

        file.close();
      }
      else
      {
        std::cerr << "Unable to open: " << filename << std::endl;
      }

      return ok;
    }


    // -- Output the result -----------------------------------------------------
    void report()
    {
      std::cout << "  Waypoints: " << _waypoints.size() << std::endl;

      if (_full.active() && !_waypoints.empty())
      {
        std::cout << "    Waypoint     Latitude  Longitude Time                    Elevation" << std::fixed << std::endl;

        for (auto iter = _waypoints.begin(); iter != _waypoints.end(); ++iter)
        {
          std::cout << "    ";
          report(iter->_name, 10);
          report(iter->_lat, 10, 5);
          report(iter->_lon, 10, 5);
          report(iter->_time, 24);
          report(getDouble(iter->_ele), 8, 3);
          std::cout  << std::endl;
        }
      }

      std::cout << "  Routes:    " << _routes.size() << std::endl;
      for(auto iter = _routes.begin(); iter != _routes.end(); ++iter)
      {
        std::cout << "    Route: '" << iter->_name
                  << "' Points: " << iter->_points.size()
                  << " Distance: " << std::fixed << std::setw(8) << std::setprecision(1) << getDistance(iter->_points)
                  << std::endl;

        if (_full.active() && !iter->_points.empty())
        {
          std::cout << "        Latitude  Longitude" << std::fixed << std::endl;

          for (auto iter2 = iter->_points.begin(); iter2 != iter->_points.end(); ++iter2)
          {
            std::cout << "      ";
            report(iter2->_lat, 10, 5);
            report(iter2->_lon, 10, 5);
            std::cout << std::endl;
          }
        }
      }

      std::cout << "  Tracks:    " << _tracks.size() << std::endl;
      for (auto iter = _tracks.begin(); iter != _tracks.end(); ++iter)
      {
        std::cout << "    Track: '" << iter->_name << "' Segments: " << iter->_segments.size() << std::endl;

        int i = 1;
        for (auto iter2 = iter->_segments.begin(); iter2 != iter->_segments.end(); ++iter2, i++)
        {
          std::cout << "      Segment: " << std::setw(2) << i
                    << " Points: " << std::setw(4) << iter2->_points.size()
                    << " Distance: " << std::fixed << std::setw(8) << std::setprecision(1) << getDistance(iter2->_points);

          if (!iter2->_minTime.empty() && !iter2->_maxTime.empty())
          {
            std::cout << " Bounds: " << iter2->_minTime << "..." << iter2->_maxTime;
          }

          std::cout << std::endl;

          if (_full.active() && !iter2->_points.empty())
          {
            bool extend = _extend.active();

            std::cout << "          Latitude  Longitude                     Time Elevation   Distance";

            if (extend) std::cout << "   Speed Bearing";

            std::cout << std::fixed << std::endl;

            double prevLat = INVALID;
            double prevLon = INVALID;
            time_t prevTime = 0;

            for (auto iter3 = iter2->_points.begin(); iter3 != iter2->_points.end(); ++iter3)
            {
              std::cout << "        ";
              report(iter3->_lat, 10, 5);
              report(iter3->_lon, 10, 5);
              report(iter3->_time, 24);
              report(getDouble(iter3->_ele), 9, 1);

              double distance;
              double bearing;
              getDistanceAndBearing(prevLat, prevLon, iter3->_lat, iter3->_lon, distance, bearing);
              report(distance, 10, 1);

              if (extend)
              {
                report(getSpeed(distance, prevTime, iter3->_time), 7, 1);
                report(bearing, 7, 1);
              }

              std::cout << std::endl;
            }
          }
        }
      }
    }

    // -- Callbacks -------------------------------------------------------------

    virtual void startElement(const std::string &path, const std::string &, const Attributes &attributes)
    {
      if (path == "/gpx/wpt")
      {
        _waypoint.reset();

        _waypoint._lat = getDoubleAttribute(attributes, "lat");
        _waypoint._lon = getDoubleAttribute(attributes, "lon");
      }
      else if (path == "/gpx/rte")
      {
        _route.reset();
      }
      else if (path == "/gpx/rte/rtept")
      {
        _routepoint.reset();

        _routepoint._lat = getDoubleAttribute(attributes, "lat");
        _routepoint._lon = getDoubleAttribute(attributes, "lon");
      }
      else if (path == "/gpx/trk")
      {
        _track.reset();
      }
      else if (path == "/gpx/trk/trkseg")
      {
        _trackSegment.reset();
      }
      else if (path == "/gpx/trk/trkseg/trkpt")
      {
        _trackpoint.reset();

        _trackpoint._lat = getDoubleAttribute(attributes, "lat");
        _trackpoint._lon = getDoubleAttribute(attributes, "lon");
      }
    }

    virtual void text(const std::string &path, const std::string &text)
    {
      if (path == "/gpx/wpt/name")
      {
        _waypoint._name += text;
      }
      else if (path == "/gpx/wpt/ele")
      {
        _waypoint._ele += text;
      }
      else if (path == "/gpx/wpt/time")
      {
        _waypoint._time += text;
      }
      else if (path == "/gpx/rte/name")
      {
        _route._name += text;
      }
      else if (path == "/gpx/trk/name")
      {
        _track._name += text;
      }
      else if (path == "/gpx/trk/trkseg/trkpt/ele")
      {
        _trackpoint._ele += text;
      }
      else if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        _trackpoint._time += text;
      }
    }

    virtual void endElement(const std::string &path, const std::string &)
    {
      if (path == "/gpx/wpt")
      {
        XMLParser::trim(_waypoint._name);
        XMLParser::trim(_waypoint._ele);
        XMLParser::trim(_waypoint._time);

        _waypoints.push_back(_waypoint);
      }
      else if (path == "/gpx/rte/rtept")
      {
        _route._points.push_back(_routepoint);
      }
      else if (path == "/gpx/rte")
      {
        XMLParser::trim(_route._name);

        _routes.push_back(_route);
      }
      else if (path == "/gpx/trk")
      {
        XMLParser::trim(_track._name);

        _tracks.push_back(_track);
      }
      else if (path == "/gpx/trk/trkseg")
      {
        _track._segments.push_back(_trackSegment);
      }
      else if (path == "/gpx/trk/trkseg/trkpt")
      {
        XMLParser::trim(_trackpoint._ele);
        XMLParser::trim(_trackpoint._time);

        if (_trackpoint._time.size() >= 18)
        {
          if (_trackSegment._minTime.empty() || _trackSegment._minTime > _trackpoint._time) _trackSegment._minTime = _trackpoint._time;
          if (_trackSegment._maxTime.empty() || _trackSegment._maxTime < _trackpoint._time) _trackSegment._maxTime = _trackpoint._time;
        }

        _trackSegment._points.push_back(_trackpoint);
      }
    }

  // -- Privates ----------------------------------------------------------------
  private:

    double getDouble(const std::string &value)
    {
      try
      {
        if (value.empty())
        {
          return INVALID;
        }
        else
        {
          return std::stod(value);
        }
      }
      catch (...)
      {
        return INVALID;
      }
    }

    double getDoubleAttribute(const Attributes &atts, const std::string &key)
    {
      auto iter = atts.find(key);

      return iter != atts.end() ? getDouble(iter->second) : INVALID;
    }

    double getDistance(double &prevLat, double &prevLon, double lat, double lon)
    {
      double distance = INVALID;

      if ((prevLat != INVALID) && (prevLon != INVALID) && (lat != INVALID) && (lon != INVALID))
      {
        distance = gpx::calcDistance(prevLat, prevLon, lat, lon);
      }

      prevLat = lat;
      prevLon = lon;

      return distance;
    }

    void getDistanceAndBearing(double &prevLat, double &prevLon, double lat, double lon, double &distance, double &bearing)
    {
      distance = INVALID;
      bearing = INVALID;

      if ((prevLat != INVALID) && (prevLon != INVALID) && (lat != INVALID) && (lon != INVALID))
      {
        distance = gpx::calcDistance(prevLat, prevLon, lat, lon);
        bearing = gpx::rad2deg(gpx::calcBearing(prevLat, prevLon, lat, lon)) + 180.0;
      }

      prevLat = lat;
      prevLon = lon;
    }


    double getSpeed(double distance, time_t &prevTime, const std::string &curTimeStr)
    {
      time_t curTime = 0;
      double speed = INVALID;

      if (!curTimeStr.empty())
      {
        std::string timeStr = curTimeStr;

        std::size_t index = timeStr.find(".");

        if (index != std::string::npos)
        {
          timeStr.erase(index, 4); // remove .000 millis
        }

        struct tm fields;

        memset(&fields, 0, sizeof(fields));

        if (strptime(timeStr.c_str(), "%FT%T%z", &fields) != nullptr)
        {
          curTime = mktime(&fields) - ::timezone;

          if ((prevTime != 0) && (prevTime != curTime) && (distance != INVALID))
          {
            speed = distance / (curTime - prevTime) * 3.6; // km/h
          }
        }
      }

      prevTime = curTime;

      return speed;
    }

    struct Trackpoint;
    double getDistance(const std::vector<Trackpoint> &points)
    {
      double distance = 0.0;
      double prevLat  = INVALID;
      double prevLon  = INVALID;

      for (auto point = points.begin(); point != points.end(); ++point)
      {
        distance += getDistance(prevLat, prevLon, point->_lat, point->_lon);
      }

      return distance;
    }

    struct Routepoint;
    double getDistance(const std::vector<Routepoint> &points)
    {
      double distance = 0.0;
      double prevLat  = INVALID;
      double prevLon  = INVALID;

      for (auto point = points.begin(); point != points.end(); ++point)
      {
        distance += getDistance(prevLat, prevLon, point->_lat, point->_lon);
      }

      return distance;
    }

    void report(double value, int width, int precision)
    {
      if (value == INVALID)
      {
        std::cout << std::setw(width) << ' ';
      }
      else
      {
        std::cout << std::setw(width) << std::setprecision(precision) << value;
      }
      std::cout << ' ';
    }

    void report(const std::string &value, size_t width)
    {
      std::cout << std::left;
      if (value.size() > width)
      {
        std::cout << value.substr(0, width-3) << "...";
      }
      else
      {
        std::cout << std::setw(width) << value;
      }
      std::cout << ' ';
      std::cout << std::right;
    }

    // -- Members ---------------------------------------------------------------
    arg::Arguments           _arguments;

    arg::Argument            _summary;
    arg::Argument            _full;
    arg::Argument            _extend;

    XMLParser                _xmlParser;

    std::string   _path;

    struct Waypoint
    {
      void reset()
      {
        _name.clear();
        _lat = INVALID;
        _lon = INVALID;
        _ele.clear();
        _time.clear();
      }

      std::string _name;
      double      _lat;
      double      _lon;
      std::string _ele;
      std::string _time;
    };
    Waypoint _waypoint;

    std::vector<Waypoint> _waypoints;

    struct Routepoint
    {
      void reset()
      {
        _lat = INVALID;
        _lon = INVALID;
      }

      double      _lat;
      double      _lon;
    };
    Routepoint _routepoint;

    struct Route
    {
      void reset()
      {
        _name.clear();
        _points.clear();
      }

      std::string             _name;
      std::vector<Routepoint> _points;
    };
    Route _route;

    std::vector<Route> _routes;

    struct Trackpoint
    {
      void reset()
      {
        _lat = INVALID;
        _lon = INVALID;
        _ele.clear();
        _time.clear();
      }

      double      _lat;
      double      _lon;
      std::string _ele;
      std::string _time;
    };
    Trackpoint _trackpoint;

    struct TrackSegment
    {
      void reset()
      {
        _minTime.clear();
        _maxTime.clear();
        _points.clear();
      }

      std::string             _minTime;
      std::string             _maxTime;
      std::vector<Trackpoint> _points;
    };
    TrackSegment _trackSegment;

    struct Track
    {
      void reset()
      {
        _name.clear();
        _segments.clear();
      }

      std::string               _name;
      std::vector<TrackSegment> _segments;
    };
    Track _track;

    std::vector<Track> _tracks;
  };
}

// -- Main program ------------------------------------------------------------

int main(int argc, char *argv[])
{
  gpxtools::GPXLs gpxLs;

  if (gpxLs.parseArguments(argc, argv))
  {
    gpxLs.report();

    return 0;
  }

  return 1;
}
